package alibaba

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"

	"github.com/git-lfs/pktline"
	"github.com/olekukonko/tablewriter"

	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/protocol"
	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/utils"
	log "golang.aliyun-inc.com/agit/alog/alogcore"
	pb "golang.aliyun-inc.com/agit/satellite-proto/go"
)

// NullSHA1 represent the null oid
const (
	NullSHA1           = "0000000000000000000000000000000000000000"
	ClientCreateMethod = "COMMAND_LINE"
	Codeup             = "Codeup"
	AtomGit            = "AtomGit"
)

// CodeupHook struct
type CodeupHook struct {
	pktline  *pktline.Pktline
	isMaster bool // set true to enable agitflow handling
	router   CommandRouter
	handler  CommandHandler
	crAPI    CodeReviewAPI
	stderr   io.Writer
	platform string

	negores   *protocol.NegotiateResult
	commands  []*protocol.Command
	pushopts  *[]protocol.PushOption
	callbacks *[]func(error)
	votes     []*pb.Command
}

// NewCodeupHook create alibaba agitflow featured proc-receive
func NewCodeupHook(stdin io.Reader, stdout, stderr io.Writer, api API, crAPI CodeReviewAPI, duplex bool, platform string, router CommandRouter, checker DivergeChecker) *CodeupHook {
	if stdin == nil {
		stdin = os.Stdin
	}
	if stdout == nil {
		stdout = os.Stdout
	}
	if stderr == nil {
		stderr = os.Stderr
	}
	if router == nil {
		router = &CodeupRouter{&NullDetector{}}
	}
	if checker == nil {
		checker = &CodeupDivergeChecker{}
	}

	callbacks := &[]func(err error){}
	pushopts := &[]protocol.PushOption{}

	handler := &CodeupCommandHandler{
		api:       api,
		crAPI:     crAPI,
		isMaster:  duplex,
		pushopts:  pushopts,
		callbacks: callbacks,
		checker:   checker,
	}

	return &CodeupHook{
		pktline:   pktline.NewPktline(bufio.NewReader(stdin), bufio.NewWriter(stdout)),
		isMaster:  duplex,
		router:    router,
		handler:   handler,
		crAPI:     crAPI,
		stderr:    stderr,
		callbacks: callbacks,
		pushopts:  pushopts,
		platform:  platform,
	}
}

// Run the proc-receive hook
func (h *CodeupHook) Run(ctx context.Context) error {
	if err := h.negotiate(ctx); err != nil {
		return fmt.Errorf("[negotiate]: %w", err)
	}

	if err := h.recvCommands(ctx); err != nil {
		return fmt.Errorf("[recv-commands]: %w", err)
	}

	if err := h.execute(ctx); err != nil {
		return fmt.Errorf("[exec-commands]: %w", err)
	}

	return nil
}

// negotiate: exchange version and push-options with receive-pack
func (h *CodeupHook) negotiate(ctx context.Context) error {
	// 1: resolve negotiation payload from receive-pack
	payloads, err := h.pktline.ReadPacketList()
	if err != nil {
		log.WithError(err).Error(ctx, "[negotiate]: read pktline err")
		return fmt.Errorf("negotiate failed: read pktline err: %w", err)
	}

	if len(payloads) == 0 {
		return fmt.Errorf("negotiate failed: no negotiation received")
	}

	payload := payloads[0]
	if !strings.HasPrefix(payload, "version=") {
		return fmt.Errorf("negotiate failed: invalid negotiation payload %q", payload)
	}

	h.negores, err = protocol.ParseNegotiation(payload)
	if err != nil {
		return err
	}

	// 2: send our negotiation to receive-pack
	if err = h.pktline.WritePacketText(fmt.Sprintf("version=%s\000%s", h.negores.Version, h.negores.GetHookCapabilities())); err != nil {
		return fmt.Errorf("write negotiation failed: %w", err)
	}
	if err = h.pktline.WriteFlush(); err != nil {
		return fmt.Errorf("flush negotiation failed: %w", err)
	}

	return nil
}

// recvCommands: receive commands and push-options from receive-pack
func (h *CodeupHook) recvCommands(ctx context.Context) error {
	payloads, err := h.pktline.ReadPacketList()
	if err != nil {
		return fmt.Errorf("recv commands: read pktline err: %w", err)
	}

	for _, payload := range payloads {
		payload = strings.TrimSuffix(payload, "\n")
		cmdSplits := strings.Split(payload, " ")
		if len(cmdSplits) != 3 {
			return fmt.Errorf("recv command: illegal command from receive-pack %q", payload)
		}

		cmd, err := protocol.NewCommand(protocol.GitCommand{
			OldOID: cmdSplits[0],
			NewOID: cmdSplits[1],
			Ref:    cmdSplits[2],
		})
		if err != nil {
			return fmt.Errorf("recv command: illegal command from receive-pack %q: %w", payload, err)
		}

		h.commands = append(h.commands, cmd)
	}

	// resolve push-options payload if supported
	// if server and hook both support "push-options", then read the push options
	if h.negores.HookCapabilities["push-options"] && h.negores.ServerCapabilities["push-options"] {
		if payloads, err = h.pktline.ReadPacketList(); err != nil {
			return fmt.Errorf("recv push options: read pktline err: %w", err)
		}

		for _, payload := range payloads {
			payload = strings.TrimSuffix(payload, "\n")
			optionSplits := strings.SplitN(payload, "=", 2)
			// kv pair
			if len(optionSplits) == 2 {
				*h.pushopts = append(*h.pushopts, protocol.PushOption{
					Name:  optionSplits[0],
					Value: optionSplits[1],
				})
			} else if len(optionSplits) == 1 { // only key
				*h.pushopts = append(*h.pushopts, protocol.PushOption{
					Name:  optionSplits[0],
					Value: "",
				})
			} else {
				return fmt.Errorf("recv command: illegal push option from receive-pack %q", payload)
			}
		}
	}

	return nil
}

func (h *CodeupHook) execute(ctx context.Context) error {
	var (
		pbcmd         *pb.Command
		err           error
		commandList   []*protocol.Command
		detailUrl     = ""
		useCodeReview = false
		projectId     = ""
		pushOpts      = make(map[string]string)
	)

	for _, opt := range *h.pushopts {
		pushOpts[opt.Name] = opt.Value
	}

	if h.platform == Codeup {
		useCodeReviewValue, _ := os.LookupEnv(UseCodeReviewKey)
		projectId, _ = os.LookupEnv(ProjectIDKey)
		useCodeReview = useCodeReviewValue == "true"
	} else if h.platform == AtomGit {
		useCodeReview = true
	}

	refSetting, _ := h.crAPI.QueryRefReviewSetting(ctx, projectId)

	for _, command := range h.commands {
		pbcmd = nil
		err = nil
		switch h.router.routeCommand(ctx, command, pushOpts, refSetting) {
		case "agit-flow":
			if useCodeReview {
				pbcmd, err = h.handler.handleAgitFlowCR(ctx, command)
			} else {
				pbcmd, err = h.handler.handleAgitFlow(ctx, command)
			}
			if err != nil {
				return fmt.Errorf("process agitflow ref %q failed: %w", command.Ref, err)
			}
		case "trunk-base":
			if useCodeReview {
				pbcmd, err = h.handler.handleTrunkBaseCR(ctx, command, pushOpts)
			} else {
				pbcmd, err = h.handler.handleTrunkBase(ctx, command, pushOpts)
			}
			if err != nil {
				return fmt.Errorf("process trunk base ref %q failed: %w", command.Ref, err)
			}
		case "normal":
			pbcmd, err = h.handler.handleNormal(command)
			if err != nil {
				return fmt.Errorf("process normal ref %q failed: %w", command.Ref, err)
			}
		case "ref":
			commandList = append(commandList, command)
			continue
		}

		if pbcmd != nil {
			log.Info(ctx, "[exec-commands]",
				log.NewStringField("command", command.String()),
				log.NewStringField("vote", strings.Join([]string{pbcmd.GetOldOid(), pbcmd.GetNewOid(), string(pbcmd.GetRefname())}, " ")))
			h.votes = append(h.votes, pbcmd)
		}
	}

	if len(commandList) > 0 {
		pbcmd, err = h.handler.handleRefReview(ctx, commandList, &detailUrl)
		if err != nil && strings.HasPrefix(err.Error(), "无法完成此操作！存在进行中的合并请求") {
			matches := pattern.FindAllStringSubmatch(err.Error(), -1)
			url := ""
			if len(matches) > 0 && len(matches[0]) > 2 {
				url = matches[0][2]
			}
			utils.SetCommandsNGReason(commandList, "create ref-review failed")
			utils.SetCommandsHint(commandList, "已存在相同的引用评审\n"+url)
			return nil
		}
		if err != nil {
			utils.SetCommandsNGReason(commandList, err.Error())
			return fmt.Errorf("create code review for branches and tags failed: %w", err)
		}
		message := fmt.Sprintf("A ref review has been created, please visit:\n%s", detailUrl)
		if err := utils.PrepareCommandsSuccess(commandList, pbcmd, message); err != nil {
			utils.SetCommandsNGReason(commandList, err.Error())
			return fmt.Errorf("create code review for branches and tags failed: %w", err)
		}
		h.votes = append(h.votes, pbcmd)
	}

	return nil
}

// Report sends hook result to receive-pack and user
func (h *CodeupHook) Report(ctx context.Context) error {
	table := tablewriter.NewWriter(h.stderr)
	table.SetAutoWrapText(false)
	table.SetHeader([]string{fmt.Sprintf("The following tips are provided by %s:", h.platform)})
	table.SetHeaderLine(true)
	table.SetAutoFormatHeaders(false)
	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
	table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgYellowColor})
	for _, cmd := range h.commands {
		hint := cmd.GetHint()
		if len(strings.TrimSpace(hint)) > 0 {
			if cmd.GetStatus() == protocol.CommandStatusNG {
				appendMessageWithColor(table, hint, tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor})
				continue
			}
			appendMessageWithColor(table, hint, tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor})
		}
	}
	if table.NumLines() > 0 {
		table.Render()
	}

	for _, cmd := range h.commands {
		for _, report := range cmd.GenerateReports() {
			if err := h.pktline.WritePacketText(report); err != nil {
				return err
			}
		}
	}
	return h.pktline.WriteFlush()
}

// GetVotes get votes from pb.Command
func (h *CodeupHook) GetVotes() []*pb.Command {
	return h.votes
}

// GetCallbacks get callback functions
func (h *CodeupHook) GetCallbacks() []func(error) {
	return *h.callbacks
}

func voteSkipForSlave(command *protocol.Command) (*pb.Command, error) {
	return &pb.Command{
		Refname:    []byte(command.Ref),
		OldOid:     command.OldOID,
		NewOid:     command.NewOID,
		SkipVoting: true,
	}, nil
}

// parseMrIDOldCommit parse mrID and old-oid from push options
func parseMrIDOldCommit(pushOpts map[string]string) (string, string, error) {
	reviewVal := pushOpts["review"]
	_, err := strconv.ParseInt(reviewVal, 10, 32)
	if err != nil {
		return "", "", fmt.Errorf("invalid mrid")
	}
	oldCommit, exist := pushOpts["old-oid"]
	if exist && !utils.CheckOID(oldCommit) {
		return "", "", fmt.Errorf("invalid old-oid")
	}

	return reviewVal, oldCommit, nil
}

func appendMessageWithColor(table *tablewriter.Table, message string, color tablewriter.Colors) {
	splits := strings.Split(message, "\n")
	for _, split := range splits {
		table.Rich([]string{split}, []tablewriter.Colors{color})
	}
}
